#ifndef PRIMITIVE_H
#define PRIMITIVE_H

#include <vector>
#include <climits>
#include "opengl_inc.h"
#include <boost/math/constants/constants.hpp>
#include <cmath>

struct VertexNoData
{
  VertexNoData(const Vector4f& v = Vector4f(), const Vector4f& n = Vector4f()) : vertex(v), normal(n) {}
  Vector4f vertex;
  Vector4f normal;
  static bool hasData() { return false; }
};

struct VertexWData
{
  VertexWData(const Vector4f& v = Vector4f(), const Vector4f& n = Vector4f(), const float d = 0.0) : vertex(v), normal(n), data(d) {}
  Vector4f vertex;
  Vector4f normal;
  float data;
  static bool hasData() { return true; }
};

namespace OGLPrimitiveHelpers
// needed since C++03 does not allow local types to be a template parameters
// may be removed since c++11 allow this
{
  template <class Vertex>
  struct VerticeCmp
  {
/*    bool operator () (const Vertex& lhs, const Vertex& rhs)
    {
      return (lhs.vertex[0] < rhs.vertex[0]) ? true : (lhs.vertex[0] > rhs.vertex[0]) ? false :
             (lhs.vertex[1] < rhs.vertex[1]) ? true : (lhs.vertex[1] > rhs.vertex[1]) ? false :
             (lhs.vertex[2] < rhs.vertex[2]) ? true : (lhs.vertex[2] > rhs.vertex[2]) ? false :
             (lhs.vertex[3] < rhs.vertex[3]) ? true : (lhs.vertex[3] > rhs.vertex[3]) ? false :
             (lhs.normal[0] < rhs.normal[0]) ? true : (lhs.normal[0] > rhs.normal[0]) ? false :
             (lhs.normal[1] < rhs.normal[1]) ? true : (lhs.normal[1] > rhs.normal[1]) ? false :
             (lhs.normal[2] < rhs.normal[2]) ? true : (lhs.normal[2] > rhs.normal[2]) ? false :
             (lhs.normal[3] < rhs.normal[3]) ? true : (lhs.normal[3] > rhs.normal[3]) ? false : false;
    } */
    bool operator () (const Vertex& lhs, const Vertex& rhs)
    {
      switch ( Vector4f::compare(lhs.vertex, rhs.vertex))
      {
        case 0:
          {
            switch( Vector4f::compare(lhs.normal, rhs.normal))
            {
              case 0: return false;
              case 1: return false;
              case -1: return true;
            }
          }
        case -1: return true;
        case 1: return false;
        default: return false;
      }
    }
  };
  struct Generator
  {
    int next;
    static Generator& instance()
    {
      static Generator gen;
      return gen;
    }
    int operator () () { return next++; }
  private:
    Generator(): next(0) {}
  };

  struct Idx
  {
  public:
    int i;
    Idx() : i(Generator::instance()()) {}
    operator GLuint () const { return i; }
  };

  template <class Vertex>
  class Subdivider
  {
  public:
    Subdivider(std::vector<Vertex>& v, std::vector<GLuint>& i) : vertices(v), indices(i) {}
    void operator()(const Vector3f& v1, const Vector3f& v2, const Vector3f& v3, int level)
    {
      if (!level)
      {
        // writing data

        GLuint i = vertices.size();
        vertices.push_back(Vertex(Vector4f(v1/2.0), Vector4f(v1)));
        vertices.push_back(Vertex(Vector4f(v2/2.0), Vector4f(v2)));
        vertices.push_back(Vertex(Vector4f(v3/2.0), Vector4f(v3)));
        indices.push_back(i++); indices.push_back(i++); indices.push_back(i++);
      } else {
        Vector3f v4 = (v1 + v2).normalize();
        Vector3f v5 = (v2 + v3).normalize();
        Vector3f v6 = (v3 + v1).normalize();
        this->operator ()(v1, v4, v6, level -1);
        this->operator ()(v4, v2, v5, level -1);
        this->operator ()(v6, v5, v3, level -1);
        this->operator ()(v4, v5, v6, level -1);
      }
    }
  private:
    std::vector<Vertex>& vertices;
    std::vector<GLuint>& indices;
  };
}


template < class V = VertexNoData >
class OGLPrimitive
{
public:
  typedef V Vertex;
  static GLuint restartIndex() {return  std::numeric_limits<GLuint>::max(); }
  OGLPrimitive() : initialized(false) {}
  void init()
  {
    initialized = true;
    glGenBuffers(1, &vert);
    glGenBuffers(1, &indxs);
  }

  ~OGLPrimitive() {}
  void deInit()
  {
    if (initialized)
    {
      glDeleteBuffers(1, &vert);
      glDeleteBuffers(1, &indxs);
    }
  }
  std::vector<Vertex>& vertex_data() { return vertices; }
  std::vector<int>& index_data() { return indices; }
  
  GLenum mode() const { return type; }
  template < typename T >
  void addTriangle(const Vector3T<T>& v1, const Vector3T<T>& n1, const Vector3T<T>& v2, const Vector3T<T>& n2, const Vector3T<T>& v3, const Vector3T<T>& n3)
  {
    int i = vertices.size();
    vertices.push_back(Vertex(Vector4f(v1), Vector4f(n1)));
    vertices.push_back(Vertex(Vector4f(v2), Vector4f(n2)));
    vertices.push_back(Vertex(Vector4f(v3), Vector4f(n3)));
    if (type == GL_TRIANGLE_STRIP) indices.push_back(restartIndex());
    indices.push_back(i++); indices.push_back(i++); indices.push_back(i++);
  }
  template < typename T >
  void addTriangle(const Vector3T<T>& v1, const Vector3T<T>& n1, const float d1, const Vector3T<T>& v2, const Vector3T<T>& n2, const float d2, const Vector3T<T>& v3, const Vector3T<T>& n3, const float d3)
  {
    int i = vertices.size();
    vertices.push_back(Vertex(Vector4f(v1), Vector4f(n1), d1));
    vertices.push_back(Vertex(Vector4f(v2), Vector4f(n2), d2));
    vertices.push_back(Vertex(Vector4f(v3), Vector4f(n3), d3));
    if (type == GL_TRIANGLE_STRIP) indices.push_back(restartIndex());
    indices.push_back(i++); indices.push_back(i++); indices.push_back(i++);
  }
  void normalize()
  {
    typedef std::map<Vertex, OGLPrimitiveHelpers::Idx, OGLPrimitiveHelpers::VerticeCmp<Vertex> > map_type;
    map_type v;
    GLuint idmap[vertices.size()];
    for (int i = 0, ie = vertices.size(); i < ie; ++i)
    {
      idmap[i] = v[vertices[i]];
    }
    vertices.resize(v.size());
    for (typename map_type::const_iterator i = v.begin(), ie = v.end(); i != ie; ++i)
    {
      vertices[i->second] = i->first;
    }
    std::vector<GLuint> idxs = indices;
    for (int i = 0, ie = indices.size(); i < ie; ++i)
    {
      indices[i] = idmap[idxs[i]];
    }
  }
  void fillSphere(int detail = 2)
  {
    type = GL_TRIANGLES;

    const float X = .525731112119133606;
    const float Z = .850650808352039932;
    Vector3f vdata[12] = {
      Vector3f(-X, 0.0, Z), Vector3f(X, 0.0, Z), Vector3f(-X, 0.0, -Z),
      Vector3f(X, 0.0, -Z), Vector3f(0.0, Z, X), Vector3f(0.0, Z, -X),
      Vector3f(0.0, -Z, X), Vector3f(0.0, -Z, -X), Vector3f(Z, X, 0.0), 
      Vector3f(-Z, X, 0.0), Vector3f(Z, -X, 0.0), Vector3f(-Z, -X, 0.0)
                         };
    int tindices[20][3] = {
      {1,4,0}, {4,9,0}, {4,5,9}, {8,5,4}, {1,8,4},
      {1,10,8}, {10,3,8}, {8,3,5}, {3,2,5}, {3,7,2},
      {3,10,7}, {10,6,7}, {6,11,7}, {6,0,11}, {6,1,0},
      {10,1,6}, {11,0,9}, {2,11,9}, {5,2,9}, {11,2,7}
                          };
    OGLPrimitiveHelpers::Subdivider<Vertex> subdivider(vertices, indices);
    for (int i = 0; i < 20; ++i)
    {
      subdivider(vdata[tindices[i][0]], vdata[tindices[i][1]], vdata[tindices[i][2]], detail);
    }
    normalize();
  }
  void fillCube()
  {
  // crates cube in origin with the sidelength of 1
    type = GL_TRIANGLES;
    vertices.reserve(24);
    indices.reserve(30);
    Vector4f v[] = { 
               Vector4f( 0.5, -0.5,  0.5, 1.0), Vector4f( 0.5,  0.5,  0.5, 1.0),
               Vector4f(-0.5,  0.5,  0.5, 1.0), Vector4f(-0.5, -0.5,  0.5, 1.0),
               Vector4f(-0.5, -0.5, -0.5, 1.0), Vector4f(-0.5,  0.5, -0.5, 1.0),
               Vector4f( 0.5,  0.5, -0.5, 1.0), Vector4f( 0.5, -0.5, -0.5, 1.0)
                   };
    Vector4f n[] = {
               Vector4f( 1.0,  0.0,  0.0, 1.0), Vector4f( 0.0,  1.0,  0.0, 1.0),
               Vector4f( 0.0,  0.0,  1.0, 1.0), Vector4f(-1.0,  0.0,  0.0, 1.0),
               Vector4f( 0.0, -1.0,  0.0, 1.0), Vector4f( 0.0,  0.0, -1.0, 1.0)
                   };
    
    vertices.push_back(Vertex(v[0], n[2])); // 0
    vertices.push_back(Vertex(v[3], n[2])); // 1
    vertices.push_back(Vertex(v[1], n[2])); // 2
    vertices.push_back(Vertex(v[2], n[2])); // 3
    indices.push_back(0); indices.push_back(1); indices.push_back(2);
    indices.push_back(1); indices.push_back(2); indices.push_back(3);
    
    vertices.push_back(Vertex(v[3], n[3])); // 4
    vertices.push_back(Vertex(v[4], n[3])); // 5
    vertices.push_back(Vertex(v[2], n[3])); // 6
    vertices.push_back(Vertex(v[5], n[3])); // 7
    indices.push_back(4); indices.push_back(5); indices.push_back(6);
    indices.push_back(5); indices.push_back(6); indices.push_back(7);
    
    vertices.push_back(Vertex(v[0], n[4])); // 8
    vertices.push_back(Vertex(v[7], n[4])); // 9
    vertices.push_back(Vertex(v[3], n[4])); //10
    vertices.push_back(Vertex(v[4], n[4])); //11
    indices.push_back(8); indices.push_back(9); indices.push_back(10);
    indices.push_back(9); indices.push_back(10); indices.push_back(11);
    
    vertices.push_back(Vertex(v[4], n[5])); //12
    vertices.push_back(Vertex(v[7], n[5])); //13
    vertices.push_back(Vertex(v[5], n[5])); //14
    vertices.push_back(Vertex(v[6], n[5])); //15
    indices.push_back(12); indices.push_back(13); indices.push_back(14);
    indices.push_back(13); indices.push_back(14); indices.push_back(15);
    
    vertices.push_back(Vertex(v[0], n[0])); //16
    vertices.push_back(Vertex(v[1], n[0])); //17
    vertices.push_back(Vertex(v[7], n[0])); //18
    vertices.push_back(Vertex(v[6], n[0])); //19
    indices.push_back(16); indices.push_back(17); indices.push_back(18);
    indices.push_back(17); indices.push_back(18); indices.push_back(19);
    
    vertices.push_back(Vertex(v[1], n[1])); //20
    vertices.push_back(Vertex(v[2], n[1])); //21
    vertices.push_back(Vertex(v[6], n[1])); //22
    vertices.push_back(Vertex(v[5], n[1])); //23
    indices.push_back(20); indices.push_back(21); indices.push_back(22);
    indices.push_back(21); indices.push_back(22); indices.push_back(23);
  }
  void fillCylinder(int nSlices = 20)
  {
    // creates cylinder with length of 1 and diameter of 1
    type = GL_TRIANGLE_STRIP;
    vertices.reserve(nSlices*2);
    indices.reserve(nSlices*2+2);
    for (int i = 0; i < 2*nSlices; ++i)
    {
      float angle = 2.0*boost::math::constants::pi<float>()/nSlices*i;
      float ca = cos(angle), sa = sin(angle);
      vertices.push_back(Vertex(Vector4f(ca/2, sa/2, 0.5, 1.0), Vector4f(ca, sa, 0.0, 1.0)));
      indices.push_back(i);
      ++i;
      angle = 2.0*boost::math::constants::pi<float>()/nSlices*i;
      ca = cos(angle), sa = sin(angle);
      vertices.push_back(Vertex(Vector4f(ca/2, sa/2, -0.5, 1.0), Vector4f(ca, sa, 0.0, 1.0)));
      indices.push_back(i);
    }
    indices.push_back(0); indices.push_back(1);
  }
  void fillCone(int nSlices = 40)
  {
    // creates cone with length of 1 and diameter of 1
    type = GL_TRIANGLES;
    vertices.reserve(nSlices*2);
    indices.reserve(nSlices*3);
    float t = sqrt(1.25);
    for (int i = 0; i < nSlices; ++i)
    {
      float a1 = boost::math::constants::pi<float>()/nSlices*i;
      float a2 = boost::math::constants::pi<float>()/nSlices*(i+1);
      vertices.push_back(Vertex(Vector4f(0.0, 0.0, 0.5, 1.0),Vector4f(cos((a1+a2)/2)/t, sin((a1+a2)/2)/t, 0.5/t, 1.0)));
      vertices.push_back(Vertex(Vector4f(cos(a1)/2, sin(a1)/2, -0.5, 1.0),Vector4f(cos(a1)/t, sin(a1)/t, 0.5/t, 1.0)));
      indices.push_back(i*2); indices.push_back(i*2+1); indices.push_back(i*2+2);
    }
    *(--(indices.end())) = 0;
  }
  void filLine()
  {
    type = GL_LINES;
    vertices.push_back(Vertex(Vector4f(0.0, 0.0, 0.5, 1.0), Vector4f()));
    vertices.push_back(Vertex(Vector4f(0.0, 0.0,-0.5, 1.0), Vector4f()));
    indices.push_back(0); indices.push_back(1);
  }
  void fillPoint()
  {
    type = GL_POINTS;
    vertices.push_back(Vertex(Vector4f(0.0, 0.0, 0.0, 1.0), Vector4f()));
    indices.push_back(0);
  }
  void uploadPrimitive()
  {
    glBindBuffer(GL_ARRAY_BUFFER, vert);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &(vertices[0]), GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indxs);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), &(indices[0]), GL_STATIC_DRAW);
  }
#ifdef DEBUG
  void printData()
  {
    glBindBuffer(GL_TEXTURE_BUFFER, vert);
    Vertex* vertices_ = (Vertex*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    std::cerr << "Vertices" << std::endl;
    for (unsigned int i = 0; i < vertices.size(); ++i) std::cerr << vertices_[i].vertex << std::endl << vertices_[i].normal <<std::endl ;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer(GL_TEXTURE_BUFFER, indxs);
    GLuint* indices_ = (GLuint*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    std::cerr << "Indices" << std::endl;
    for (unsigned int i = 0; i < indices.size(); ++i) std::cerr << indices_[i] << " " ;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer(GL_TEXTURE_BUFFER, 0);
  }
#endif
  GLuint vertexBuff() const { return vert; }
  GLuint indexBuff() const { return indxs; }
  int vertices_count() const { return vertices.size(); }
  int indices_count() const { return indices.size(); }
private:
  GLenum type;
  GLuint vert, indxs;
  std::vector<Vertex> vertices;
  std::vector<GLuint> indices;
  bool initialized;
};

#endif
